package profile;

import static java.lang.Math.abs;
import ij.ImagePlus;
import ij.gui.NewImage;
import ij.plugin.filter.GaussianBlur;
import ij.process.ImageProcessor;

import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


import model.ProfileModel;
import controller.ProfileController;

/**
 * This class represents a profile.  Holds all the statistic information and
 * pixel values for the profile
 * 
 */
public class Profile {
    
    private static final String DEFAULT_Y_AXIS_LABEL = "Pixel Value";
    
    private ProfileController controller;
    private ProfileModel model;
    protected ImagePlus img;
    protected BufferedImage imageWithSelection;

    protected double[] arr;
    protected double[] originalArr;
    protected double[] sortedProfile = null;
    protected double[] profileDeviations = null;
    
    protected double profileMedian;
    protected double profileMAD;
    protected double profileMean;
    
    protected boolean medianIsSet = false;
    protected boolean madIsSet = false;
    private boolean meanIsSet = false;
    
    /**
     * cropInfo holds the crop information.  If the profile is uncropped, the cropInfo 
     * 'left' will be 0, and the cropInfo 'right' will be the same as the width of
     * the profile.  Making it 'protected' so SradProfile can access it
     */
    protected CropInfo cropInfo;
    
    public Profile(float[] arr, ImagePlus img, BufferedImage bimg) {
        this(floatToDoubleArray(arr));
        this.img = img;
        this.imageWithSelection = bimg;
    }
    
    public Profile(ImagePlus img, Rectangle r, BufferedImage bimg) {
        this(toProfileArray(img, r));
        this.img = img;
        this.imageWithSelection = bimg;
    }
    
    protected Profile(double[] arr) {
        this.arr = arr;
        cropInfo = new CropInfo(0, arr.length);
        controller = new ProfileController();
        model = new ProfileModel();
    }
    
    /**
     * This method converts an imagePlus into a double array, where each element
     * of the double array is the mean of the corresponding pixel column from
     * the original image.
     * 
     * @param img
     * @return
     */
    protected static double[] toProfileArray(ImagePlus img, Rectangle r) {
        byte[] pixels = (byte[]) img.getProcessor().getPixels();
        int width = img.getWidth();

        int offset, i;
        double[] resultArr = new double[r.width];
        for (int y = r.y; y < (r.y + r.height); y++) {
            offset = y * width;
            for (int x = r.x; x < (r.x + r.width); x++) {
                i = offset + x;

                //Java doesn't have unsigned bytes, so we are converting
                //the byte values into unsigned byte values here.
                int val = pixels[i];
                if (val < 0) {
                    val = val & 0xFF;
                }
                resultArr[x - r.x] += val;
            }
        }

        for (int j = 0; j < resultArr.length; j++) {
            resultArr[j] = resultArr[j] / r.height;
        }

        return resultArr;
    }
    
    
    protected static double[] floatToDoubleArray(float[] arr) {
        double[] result = new double[arr.length];
        for(int i = 0; i < arr.length; i++) {
            result[i] = arr[i];
        }
        return result;
    }

    /**
     * Returns the median absolute deviation of the profile.  Returns -1 if the 
     * profile is null
     * 
     * @return
     */
    public double getProfileMAD() {
        if(!medianIsSet || sortedProfile == null) {
            setProfileMedian();
        }
        
        if(!madIsSet || profileDeviations == null) {
            setProfileMAD();
        }
        
        return profileMAD;
    }
    
    /**
     * Sets profile MAD
     */
    protected void setProfileMAD() {
        profileDeviations = new double[cropInfo.width];
        
        for(int i = 0; i < profileDeviations.length; i++) {
            profileDeviations[i] = abs(profileMedian - sortedProfile[i]);
        }
        
        Arrays.sort(profileDeviations);
        profileMAD = profileDeviations[profileDeviations.length/2];
        
        madIsSet = true;
    }

    /**
     * Returns profile median
     * 
     * @return
     */
    public double getProfileMedian() {
        if(!medianIsSet) {
            setProfileMedian();
        }
        return profileMedian;
    }
    
    /**
     * Returns profile mean
     * 
     * @return
     */
    public double getProfileMean() {
        if(!meanIsSet ) {
            setProfileMean();
        }
        return profileMean;
    }
    
    //TODO: Think about how to do this without copying an array, 
    //      or delaying the arraycopy til later
    protected void setProfileMedian() {
        
        sortedProfile = new double[cropInfo.width];
        System.arraycopy(arr, cropInfo.left, sortedProfile, 0, cropInfo.width);
        Arrays.sort(sortedProfile);
        
        if (sortedProfile.length % 2 == 0) {
            profileMedian = (sortedProfile[sortedProfile.length/2] + sortedProfile[sortedProfile.length/2 - 1]) / 2;
        } else {
            profileMedian = sortedProfile[sortedProfile.length/2];
        }
        medianIsSet = true;
    }
    
    //TODO: These 'set' methods should all be tied to the Profile model so that
    // if they are changed (for example after a crop) all of the profile model
    // listeners automatically update
    private void setProfileMean() {
        double sum = 0;
        for(int i = cropInfo.left; i < cropInfo.right; i++) {
            sum += arr[i];
        }
        
        profileMean = sum/cropInfo.width;
        meanIsSet = true;
    }

    public double[] getArr() {
        return arr;
    }
    
    public void setArray(int[] newArray, int start, int width) {
        for (int i = 0; i < width; i++) {
            arr[start + i] = newArray[i];
        }
        resetProfileState();
    }
    
    public int getWidth() {
        return arr.length;
    }
    
    public double getProfileMax() {
        double max = 0;
        for(int i = 0; i < arr.length; i++) {
            if(arr[i] > max) 
                max = arr[i];
        }
        return max;
    }
    
    public double getCroppedProfileMax() {
        double max = 0;
        for(int i = cropInfo.left; i < cropInfo.right; i++) {
            if(arr[i] > max) 
                max = arr[i];
        }
        return max;
    }
    
    public double getProfileMin() {
        double min = Double.MAX_VALUE;
        for(int i = 0; i < arr.length; i++) {
            if(arr[i] < min)
                min = arr[i];
        }
        return min;
    }
    
    public double getCroppedProfileMin() {
        double min = Double.MAX_VALUE;
        for(int i = cropInfo.left; i < cropInfo.right; i++) {
            if(arr[i] < min)
                min = arr[i];
        }
        return min;
    }
    
    public int[] getRange(int startIndex, int width) {
        
        if (startIndex + width > arr.length){
            width = arr.length - startIndex;
        }
        
        int[] result = new int[width];
        for(int i = 0; i < width; i++) {
            result[i] = (int) arr[startIndex + i];
        }
        
        return result;
    }
    
    /**
     * Returns a buffered image of the original median image with the
     * selection.
     * 
     * @return
     */
    public BufferedImage getImageWithSelection() {
        return imageWithSelection;
    }
    
    /**
     * Returns a list of <code>Dip</code>s that are <b>under</b> the current
     * number of MADs.  So even though it's returning a list of <code>Dip</code>
     * it's actually dips.
     * 
     * @param numMads
     * @return
     */
    public List<Dip> getDips(int numMads) {
        if(!madIsSet) {
            setProfileMAD();
        }
        
        double threshold = profileMedian - ((double)numMads * profileMAD);
        
        List<Dip> dips = new ArrayList<Dip>();
        int dipStart = -1;
        boolean inDip = false;
        for(int i = cropInfo.left; i < cropInfo.right; i++) {
            if(isDipStart(i, threshold, inDip)) {
                inDip = true;
                dipStart = i;
            } else if (isDipEnd(i, threshold, inDip)) {
                inDip = false;
                dips.add(new Dip(dipStart, i, arr));
            }
        }
        
        return dips;
    }
    
    private boolean isDipStart(int i, double threshold, boolean inDip) {
        int left = (cropInfo.left > 0) ? cropInfo.left : 0; 
        if ((arr[i] <= threshold) && (!inDip || i == left)) {
            return true;
        } 
        return false;
    }
    
    private boolean isDipEnd(int i, double threshold, boolean inDip) {
        int right = (cropInfo.right < arr.length) ? (cropInfo.right-1) :(arr.length-1);  
        if((i == right) && inDip) {
            return true;
        } else if(arr[i] >= threshold && inDip) {
            return true;
        }
        return false;
    }
    
    public int getImageWidth() {
    	return img.getWidth();
    }
    
    /**
     * This method is used to "crop" the Profile.  This sets the cropLeftPosition
     * and cropRightPosition variables.  
     * 
     * @param startX
     * @param width
     */
    public void cropProfile(int startX, int width) {
        resetProfileState();
        cropInfo = new CropInfo(startX, width);
    }

    /**
     * Resets the profile state by resetting median, MAD, and mean
     */
    private void resetProfileState() {
        medianIsSet = false;
        madIsSet = false;
        meanIsSet = false; 
    }
    
    /**
     * Resets the crop.  Resetting the crop will set the profiles CropInfo
     * to have 'left' of 0 and 'width' that is equal to the profile
     */
    public void resetCrop() {
        cropProfile(0, arr.length);
    }
    
    CropInfo getCropInfo() {
        return cropInfo;
    }
    
    /**
     * Given a range, (defined by x and width) this performs a low pass filter,
     * or Gaussian blur on the selected portion of the profile with the intent
     * of getting rid of double peaks.
     * 
     * @param profileResultLeftPanel TODO
     * @param x
     * @param width
     */
    protected void performLowPassFilter(int x, int width, double radius) {
        //before doing the filter, make a "backup" of the original so we can reset
        //to the original if the user wants to
        if (originalArr == null) {
            originalArr = new double[arr.length];
            System.arraycopy(arr, 0, originalArr, 0, arr.length);
        }
            
        // per email from pcarson, construct an image one to 10 pixels high,
        // with the selection of the profile in each row then run the ImageJ
        // Gaussian Blur filter on it
        int height = 10;
        ImageProcessor ip = createImageForFilter(width, height);
        
        int[] rangeSelected = getRange(x, width);
        for(int i = 0; i < height; i++) {
            ip.putRow(0, i, rangeSelected, width);
        }
        
        doGaussianBlur(ip, radius);
        
        //put filtered values back into profile
        int[] result = new int[width];
        //using the 5th row here, but we could use any row
        ip.getRow(0, 5, result, width);
        setArray(result, x, width);
    }
    
    /**
     * If a Low Pass Filter has been performed on the Profile, calling this method
     * will reset (or undo) the filter
     */
    public void resetLowPassFilter() {
        System.arraycopy(originalArr, 0, arr, 0, arr.length);
    }
    
    protected static ImageProcessor createImageForFilter(int width, int height) {
        ImagePlus filterImage = NewImage.createByteImage("Low Pass Filter Image", width, height, 1, NewImage.FILL_WHITE);
        return filterImage.getProcessor();
    }
    
    protected static void doGaussianBlur(ImageProcessor ip, double radius) {
        //now run gaussian blur on the image
        GaussianBlur gb = new GaussianBlur();
        //use sigmaY of 0 since each row is the same thing
        gb.blurGaussian(ip, radius, 0, .0002);
    }
    
    public ProfileController getController() {
        return controller;
    }
    
    public ProfileModel getModel() {
        return model;
    }
    
    /**
     * This method returns the Y Axis label that will be on the profile's chart
     * 
     * @return
     */
    public String getYAxisLabel() {
       return DEFAULT_Y_AXIS_LABEL; 
    }
    
    /**
     * Returns the suggested filename for the exported profile
     * 
     * @return
     */
    public String getSuggestedFilename() {
        String suggestedFilename = null;
        String imageFilename = ProfileHelper.getFileName(img);
        if (imageFilename.indexOf(".") > 0) {
            suggestedFilename = imageFilename.substring(0, imageFilename.indexOf("."));
        } else {
            suggestedFilename = imageFilename;
        }
        suggestedFilename += "_profile";
        return suggestedFilename;
    }
    
    public String getOriginalImageFilename() {
        return ProfileHelper.getFileName(img);
    }
    
    /**
     * CropInfo is used to store crop information if the Profile
     * gets cropped
     * 
     * @author cshanes
     *
     */
    class CropInfo {
        
        int left;
        int right;
        int width;
        
        CropInfo(int left, int width) {
            this.left = left;
            this.right = left + width;
            this.width = width;
        }
    }



}
